Responder chain
めちゃくちゃわかりやすいので、以下を読んだ方が良い
https://swiftrocks.com/understanding-the-ios-responder-chain.html
概要
iOS アプリケーションは、その外界からユーザ操作として様々な種類の イベント を受け取る。シングルタップやダブルタップ、スワイプ等のジェスチャや、キーボードによる入力、ロックスクリーンや外部端末からの Remote Control 等がこのイベントに相当する。このイベントを適切に検知し、ハンドリングする必要がある。
発生したイベントを受け取ることができるオブジェクトは特に responder objects と呼ばれる。responder object は UIResponder クラスのインスタンスを指し、UIView や UIViewController、UIApplication 等の多くの基底クラスが UIResponder のサブクラスとなっている。
アプリケーションがイベントを検知すると、UIKit は内部的に UIEvent ンスタンスを生成し、それをイベントキュー (UIApplication.shared.sendEvent()) に push する。その後 UIKit は UIEvent を pop し、そのイベントをハンドリング可能な responder object を決定し、イベントを送信する。ここで最初にイベントを送信される responder object が first responder と呼ばれる。
UIKit による first responder の決定方法は、イベントの種類によって異なる。例えば、タッチイベント
first responder を初め、responder object がイベントを受け取る場合、イベント種別に対応した I/F が呼び出される。例えば、タッチイベントであれば touchesBegan(_:with:), touchesMoved(_:with:), touchesEnded(_:with:), touchesCancelled(_:with:) などが呼び出される。これらのイベントハンドリング用の I/F のデフォルト実装は 次の responder object にイベントを forward する といったないようになっている。そのため、responder object 内でこれらを override しなかった場合、イベントが発生してもそれらが検知されることはなく、次に通知されるべき responder object に forward される。この時の responder object 間のイベントの forwarding の連なり が responder chain と呼ばれる。
code:swift
open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?)
open func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?)
open func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesChanged(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func pressesCancelled(_ presses: Set<UIPress>, with event: UIPressesEvent?)
open func motionBegan(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func motionCancelled(_ motion: UIEvent.EventSubtype, with event: UIEvent?)
open func remoteControlReceived(with event: UIEvent?)
https://developer.apple.com/documentation/uikit/uiresponder
発生したイベントを最初に検知するのが、そのイベントをハンドリングさせたい対象のオブジェクトであるとは限らない。基本的にはイベントは View Hierarchy の下位層から順に流れていくことになるが、表側の View でタッチイベントを受け取っても、その裏側にある別の View でイベントを受け取りたいケースも十分考えられる。
このように、どの responder object が最初にイベントをハンドリングするか? を決定するために、各 responder object はイベントを受け取った時、自身がそのイベントをハンドリングすべきかどうかを判断し、ハンドリングすべきでなければ次の responder object に受け渡していく。この時辿られる responder chain のツリーが responder chain と呼ばれ、イベントを最初に受け取る responder object のことを first responder と呼ぶ。
First Responder の決定方法
UIKit は、発生したイベント種別によって first responder を決定する。
タッチイベントの first responder
https://swiftrocks.com/understanding-the-ios-responder-chain.html
table:first responder
Event Type First Responder
タッチ タッチが発生したView
プレス フォーカスされているオブジェクト
シェイク 開発者ないしUIKitが指定したオブジェクト
Remote Control 開発者ないしUIKitが指定したオブジェクト
ユーザが何か操作を行いイベントを発生させた場合、
https://developer.apple.com/library/archive/documentation/General/Conceptual/Devpedia-CocoaApp/Responder.html
https://www.hackingwithswift.com/example-code/system/what-is-the-first-responder
https://stackoverflow.com/questions/27489522/what-is-a-formal-definition-of-a-first-responder-in-ios
とは?
Responder objects は、イベントの生データを受け取り、イベント処理を行うか、他の Responder objects にフォワードする
アプリケーションがイベントを受け取ると、UIKit は適切な Responder objects にイベントを渡す
この 適切な Responder objects が First responder である
イベントの種別によって、イベントを受け取る First Responder は異なる。割愛。
Control について
Control は、そのターゲットと action message を通して直接やり取りする
action message はイベントではないが、Responder chain を利用する場合もある
Control のターゲットオブジェクトが nil だった場合、適切な action method を実装したオブジェクトが見つかるまで Responder chain を辿っていく
タッチイベントを保持する Responder の決定
UIKit は、どこでタッチイベントが発生したか?を決定するために、view ベースの hit-testing を利用する
view hierarchy 内の UIView オブジェクトの hitTest(_:with:) メソッドを呼び出し、もっとも深い位置の subview を探し、それを First responder とする
タッチ位置がある view の範囲外だった場合、hitTest(_:with:) メソッドはその view と全ての subview を無視する
hitTest
view hierarchy のなかで、特定の point を含んだもっとも遠い子孫を返す
各 subview の point(inside:with:) メソッドを呼び出し、どの subview がタッチイベントを受け取るべきか決める
point(inside:with:) が true を返した場合、その subview の view hierarchy がさらに探索される。最終的に、もっともフロントに近く、point を含んだ view が返される
以下は無視される
アルファが 0.01 以下
ユーザインタラクションが無効化されている
hidden 状態
このメソッドは view 上のコンテンツは気にしないので、コンテンツが描画されていない場所がタッチされても反応する
code:swift
func hitTest(_ point: CGPoint,
with event: UIEvent?) -> UIView?
https://developer.apple.com/documentation/uikit/uiview/1622469-hittest
UIView とタッチイベント
UIKit は、タッチ開始時に UITouch オブジェクトを作成し、view に紐付ける
タッチの状態が変化したら、UIKit は同一の UITouch オブジェクトを更新し続ける
タッチ位置が元の view から外れたとしても、view プロパティは変わらない
タッチが終了したら、UIKit は UITouch オブジェクトをリリースする
Responder chain の代替
Responder objects の next プロパティをオーバーライドできる
これをすると、次の responder はここで返したオブジェクトになる
UIKit の多くのクラスはすでにこれをオーバーライドし、特定のオブジェクトを返している
UIView
view が ViewController の root view であった場合、次の responder は ViewController になる。然もなくば、view の superview になる
UIViewController
ViewController の view が window の root view であった場合、次の responder は window オブジェクトになる
ViewController が別の ViewController によって描画されている場合、直前の ViewController になる
UIWindow
UIApplication オブジェクトになる
UIApplication
AppDelegate になる
https://developer.apple.com/documentation/uikit/touches_presses_and_gestures/using_responders_and_the_responder_chain_to_handle_events